package com.inepex.inegraphics.impl.client;

import java.util.ArrayList;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.CanvasGradient;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Widget;
import com.inepex.inegraphics.impl.client.canvas.CanvasWidget;
import com.inepex.inegraphics.shared.Context;
import com.inepex.inegraphics.shared.DrawingArea;
import com.inepex.inegraphics.shared.LinearGradient;
import com.inepex.inegraphics.shared.RadialGradient;
import com.inepex.inegraphics.shared.gobjects.ArcShape;
import com.inepex.inegraphics.shared.gobjects.Circle;
import com.inepex.inegraphics.shared.gobjects.EllipticArc;
import com.inepex.inegraphics.shared.gobjects.GraphicalObject;
import com.inepex.inegraphics.shared.gobjects.Line;
import com.inepex.inegraphics.shared.gobjects.LineTo;
import com.inepex.inegraphics.shared.gobjects.MoveTo;
import com.inepex.inegraphics.shared.gobjects.Path;
import com.inepex.inegraphics.shared.gobjects.PathElement;
import com.inepex.inegraphics.shared.gobjects.QuadraticCurveTo;
import com.inepex.inegraphics.shared.gobjects.Rectangle;
import com.inepex.inegraphics.shared.gobjects.Text;

/**
 * 
 * A client-side {@link DrawingArea} implementation.
 * It uses our own HTML5 canvas widget to display {@link GraphicalObject}s:
 * {@link CanvasWidget} (Because it supports IE6-IE8 browsers too, due to using excanvas.js)
 * Supports deferred drawing via implementing the {@link IncrementallyUpdatable} interface.
 * 
 * @author Miklós Süveges / Inepex Ltd.
 *
 */
public class DrawingAreaGWT extends DrawingArea implements IncrementallyUpdatable{

	public static boolean isHTML5Compatible(){
		return Canvas.isSupported();		
	}

	protected CanvasWidget canvas;
	protected AbsolutePanel panel;
	protected TextPositioner textPositioner;

	protected PartialUpdateAssist partialUpdateAssist = new PartialUpdateAssist();
	
	private class PartialUpdateAssist{
		private ArrayList<GraphicalObject> partialUpdateRemainingGOs;
		private IncrementalUpdater partialUpdater;
		private int drawnGOCount;
		private int totalGOCount;
		
		public void start(IncrementalUpdater u, ArrayList<GraphicalObject> gos){
			partialUpdater = u;
			partialUpdateRemainingGOs = gos;
			totalGOCount = gos.size();
			drawnGOCount = 0;
			clear();
		}
		
		public GraphicalObject popNextGO(){
			GraphicalObject go = partialUpdateRemainingGOs.get(0);
			partialUpdateRemainingGOs.remove(0);
			drawnGOCount++;
			return go;
		}
				
		public double getTotalEstimadetProgress(){
			return  drawnGOCount / totalGOCount;
		}
		
		public boolean isProgressUnfinished(){
			return partialUpdater != null && drawnGOCount < totalGOCount; 
		}
		
		public void cancel(){
			partialUpdater = null;
		}
		
		public boolean nextUpdatePart(){
			boolean finished = !isProgressUnfinished();
			if(finished){
				return false;
			}
			long treshold = partialUpdater.getUpdatePartTresholdMS();
			long start = System.currentTimeMillis();
			long duration;
			do{
				drawGraphicalObject(popNextGO());
				duration = System.currentTimeMillis() - start;
			}
			while(duration < treshold && drawnGOCount < totalGOCount);
			if(finished = drawnGOCount == totalGOCount){
				cancel();
			}
			return !finished;
		}
		
	}

	/**
	 * Creates a {@link DrawingArea} with the given dimensions
	 * @param width px
	 * @param height px
	 */
	public DrawingAreaGWT(int width, int height) {
		super(width, height);
		this.panel = new AbsolutePanel();
		panel.setPixelSize(width, height);
		CanvasWidget canvas = new CanvasWidget(width, height);
		this.canvas = canvas;
		textPositioner = new TextPositioner(panel);
		panel.add(canvas, 0, 0);
		clear();
	}

	/**
	 * Creates a {@link DrawingArea}
	 * @param canvas {@link CanvasWidget} used for drawing
	 */
	public DrawingAreaGWT(CanvasWidget canvas) {
		super(canvas.getWidth(), canvas.getHeight());
		this.canvas = canvas;
		this.panel = new AbsolutePanel();
		panel.setPixelSize(width, height);
		panel.add(canvas, 0, 0);
		textPositioner = new TextPositioner(panel);
		clear();
	}

	public void setSize(int width, int height){
		canvas.setWidth(width);
		canvas.setHeight(height);
		//			canvas.removeFromParent();
		//			canvas = new CanvasWidget(width, height);
		//			panel.add(canvas, 0, 0);
		this.width = width;
		this.height = height;
		panel.setPixelSize(width, height);
	}

	/**
	 * Use this method to add this to a the document

	 * @return an {@link AbsolutePanel} containing a {@link CanvasWidget} and {@link Text} objects (if present)
	 */
	public Widget getWidget(){
		return panel;
	}

	/**
	 * Use this method for explicitly draw on the canvas.
	 * Use getWidget() method for adding it to the document.
	 * @return the {@link CanvasWidget} used for drawing or null
	 */
	public CanvasWidget getCanvasWidget() {
		return canvas;
	}

	@Override
	protected void clear() {
		canvas.setFillStyle("white");
		canvas.clearRect(0, 0, width, height);
		textPositioner.removeAllText();
		actualContext = null;
	}

	@Override
	protected void drawPath(Path path) {
		applyContext(path.getContext());
		ArrayList<PathElement> pathElements = path.getPathElements();
		if((path.hasFill() == false && path.hasStroke() == false )||
				pathElements.size() < 1){
			return;
		}
		canvas.beginPath();
		canvas.moveTo(path.getBasePointX(), path.getBasePointY());
		for(PathElement pe : pathElements){
			if(pe instanceof QuadraticCurveTo){
				QuadraticCurveTo qTo = (QuadraticCurveTo) pe;
				canvas.quadraticCurveTo(qTo.getControlPointX(), qTo.getControlPointY(), qTo.getEndPointX(), qTo.getEndPointY());
			}
			else if(pe instanceof LineTo){//TODO avoid instanceof -> http://code.google.com/p/inechart/issues/detail?id=18
				LineTo lTo = (LineTo) pe;
				canvas.lineTo(lTo.getEndPointX(), lTo.getEndPointY());
			}
			else if(pe instanceof MoveTo){
				MoveTo mTo = (MoveTo) pe;
				canvas.moveTo(mTo.getEndPointX(), mTo.getEndPointY());
			}
		}
		if(path.hasStroke()){
			canvas.stroke();
		}
		if(path.hasFill()){
			canvas.fill();
		}
	}

	@Override
	protected void drawRectangle(Rectangle rectangle) {
		applyContext(rectangle.getContext());
		double x = rectangle.getBasePointX(), y = rectangle.getBasePointY(), width = rectangle.getWidth(), height = rectangle.getHeight(), roundedCornerRadius = rectangle.getRoundedCornerRadius();
		canvas.beginPath();
		if(roundedCornerRadius == 0){
			if(rectangle.hasFill()){
				canvas.fillRect(x,y,width,height);
			}
			if(rectangle.hasStroke()){
				canvas.strokeRect(x,y,width,height);
			}
		}
		//we create a path
		else{
			//start just below (with the amount of the roundedCornerRadius) the upper-left point (GO's basePoint) of the rectangle
			//and draw lines clockwise
			canvas.moveTo(x, y + roundedCornerRadius);
			canvas.quadraticCurveTo(x, y, x + roundedCornerRadius, y);
			canvas.lineTo(x + width - roundedCornerRadius, y);
			canvas.quadraticCurveTo(x + width, y, x + width, y + roundedCornerRadius);
			canvas.lineTo(x + width, y + height - roundedCornerRadius);
			canvas.quadraticCurveTo(x + width, y + height, x + width - roundedCornerRadius, y + height);
			canvas.lineTo(x + roundedCornerRadius, y + height);
			canvas.quadraticCurveTo(x, y + height, x , y + height  - roundedCornerRadius);
			canvas.closePath();
			if(rectangle.hasFill()){
				canvas.fill();
			}
			if(rectangle.hasStroke()){
				canvas.stroke();
			}
		}
	}

	@Override
	protected void drawCircle(Circle circle) {
		applyContext(circle.getContext());
		canvas.beginPath();
		canvas.arc(circle.getBasePointX(), circle.getBasePointY(), circle.getRadius(), 0, Math.PI * 2, false);
		if(circle.hasFill()){
			canvas.fill();
		}
		if(circle.hasStroke()){
			canvas.stroke();
		}
	}

	@Override
	protected void drawLine(Line line) {
		applyContext(line.getContext());
		canvas.beginPath();
		canvas.moveTo(line.getBasePointX(), line.getBasePointY());
		canvas.lineTo(line.getEndPointX(), line.getEndPointY());
		canvas.stroke();
	}

	/**
	 * Sets the (changed) {@link Context} variables of the canvas.
	 * @param context
	 */
	protected void applyContext(Context context){
		if(context == null){
			return;
		}
		if(actualContext == null){
			canvas.setLineJoin("round");
			canvas.setLineCap("butt");
		}
		if(actualContext == null || context.getAlpha() != actualContext.getAlpha()){
			canvas.setGlobalAlpha(context.getAlpha());
		}
		if(context.getFillGradient() == null){
			if(actualContext == null || !context.getFillColor().equals(actualContext.getFillColor())){
				canvas.setFillStyle(context.getFillColor());
			}
		}
		else {
			setGradientOnCanvas(context.getFillGradient(), false);
		}
		if(context.getStrokeGradient() == null){
			if(actualContext == null || !context.getStrokeColor().equals(actualContext.getStrokeColor())){
				canvas.setStrokeStyle(context.getStrokeColor());
			}
		}
		else{
			setGradientOnCanvas(context.getStrokeGradient(), true);
		}
		if(actualContext == null || context.getStrokeWidth() != actualContext.getStrokeWidth()){
			canvas.setLineWidth(context.getStrokeWidth());
		}
		if(!createShadows){ //only apply shadow props if we does not use our inner shadow drawing
			if(actualContext == null || actualContext.getShadowOffsetX() != context.getShadowOffsetX()){
				canvas.setShadowOffsetX(context.getShadowOffsetX());
			}
			if(actualContext == null || actualContext.getShadowOffsetY() != context.getShadowOffsetY()){
				canvas.setShadowOffsetX(context.getShadowOffsetX());
			}
			if(actualContext == null || !actualContext.getShadowColor().equals(context.getShadowColor())){
				canvas.setShadowColor(context.getShadowColor());
			}
		}

		actualContext = context;
	}

	private void setGradientOnCanvas(LinearGradient gradient, boolean forStroke){
		CanvasGradient gradientJSO;
		if(gradient instanceof RadialGradient){
			gradientJSO = canvas.createRadialGradient(gradient.getX0(), gradient.getY0(), ((RadialGradient) gradient).getR0(), gradient.getX1(), gradient.getY1(), ((RadialGradient) gradient).getR1());
		}
		else{
			gradientJSO = canvas.createLinearGradient(gradient.getX0(), gradient.getY0(),gradient.getX1(), gradient.getY1());
		}
		for (LinearGradient.ColorStop colorStop : gradient.getColorStops()) {
			gradientJSO.addColorStop(colorStop.getStop(), colorStop.getColor());
		}
		if(forStroke){
			canvas.setStrokeStyle(gradientJSO);
		}
		else{
			canvas.setFillStyle(gradientJSO);
		}
	}

	@Override
	protected void drawText(Text text) {
		if(textPositioner != null)
			textPositioner.addText(text);
	}

	@Override
	public void update() {
		if(partialUpdateAssist.isProgressUnfinished()){
			return;
		}
		textPositioner.removeAllText();
		super.update();
	}

	@Override
	public void measureText(Text text) {
		TextPositioner.measureText(text);
	}

	@Override
	protected void drawEllipticArc(EllipticArc arc) {
		applyContext(arc.getContext());
		double vhRatio = arc.getVerticalRadius() / arc.getHorizontalRadius();
		double xScale = vhRatio > 1.0 ? 1 / vhRatio : 1.0;
		double yScale = vhRatio > 1.0 ? 1.0 : vhRatio;
		double radius = Math.max(arc.getHorizontalRadius(), arc.getVerticalRadius());

		canvas.beginPath();
		canvas.save();
		canvas.scale(xScale, yScale);
		if(arc.getArcShape() == ArcShape.Pie){
			canvas.moveTo(arc.getBasePointX() / xScale, arc.getBasePointY() / yScale);
		}
		canvas.arc(arc.getBasePointX() / xScale, arc.getBasePointY() / yScale, radius, arc.getStartAngle(), arc.getArcAngle(), false);
		switch (arc.getArcShape() ) {
		case Closed:
		case Pie:
			canvas.closePath();
			break;
		}		
		canvas.restore();
		if(arc.hasFill()){
			canvas.fill();
		}
		if(arc.hasStroke()){
			canvas.stroke();
		}

	}
	
	/**
	 * Use {@link IncrementalUpdater} to update the canvas responsively
	 * @param updater
	 */
	@Override
	public void initUpdate(IncrementalUpdater updater) {
		partialUpdateAssist.start(updater, createDrawOrderList());
	}
	
	@Override
	public boolean nextUpdatePart() {
		return partialUpdateAssist.nextUpdatePart();
	}
	
	@Override
	public void cancelUpdate() {
		partialUpdateAssist.cancel();		
	}

	@Override
	public double getProgress() {
		return partialUpdateAssist.getTotalEstimadetProgress();
	}

}